package com.hmdp.service.impl;

import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.hmdp.dto.Result;
import com.hmdp.entity.Shop;
import com.hmdp.mapper.ShopMapper;
import com.hmdp.service.IShopService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.utils.CacheClient;
import com.hmdp.utils.RedisData;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import static com.hmdp.utils.RedisConstants.*;

@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Resource
    private CacheClient cacheClient;

    /**
     * 先从redis查询商铺缓存
     * |----->命中就返回商铺信息
     * |----->未命中再从数据库里面判断
     * |---|--->要是存在就先写入redis中
     * |   |----->返回商铺信息
     * |----->如果不存在就返回404
     *
     * @param id 查询id
     * @return result
     */
    @Override
    public Result queryById(Long id) {
        //实现缓存穿透
        //Shop shop = cacheClient.queryWithPassThrough(CACHE_SHOP_KEY, id, Shop.class, this::getById, CACHE_SHOP_TTL, TimeUnit.MINUTES);
        //实现缓存击穿(时间的含义是值比如这个活动持续的时间 认为停止可能是为了更新 ) 这里为20s的时间过期
        Shop shop = cacheClient.queryWithLogicalExpire(CACHE_SHOP_KEY, id, Shop.class, this::getById, 20L, TimeUnit.SECONDS);
        if (shop == null) {
            return Result.error("店铺信息不存在！！！");
        }
        return Result.success(shop);
    }

    /*
        //线程池
        private static final ExecutorService CACHE_REBUILDER_EXECUTOR = Executors.newFixedThreadPool(10);

        //逻辑过期
        public Shop queryWithLogicalExpire(Long id) {
            String key = CACHE_SHOP_KEY + id;
            String shopJson = stringRedisTemplate.opsForValue().get(key);
            if (StrUtil.isBlank(shopJson)) {
                //不存在则返回空值
                return null;
            }
            RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
            //4.命中，需要先把json反序列化为对象
            Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
            LocalDateTime expireTime = redisData.getExpireTime();
            //5.判断是否过期
            if (expireTime.isAfter(LocalDateTime.now())) {
                //5.1.未过期，直接返回店铺信息
                System.out.println("数据没有过期");
                return shop;
            }
            //5.2.已过期、需要缓存重建
            //6.缓存重建
            //6.1.获取互斥锁
            String LockKey = LOCK_SHOP_KEY + id;
            // 6.2.判断是否获取锁成功
            boolean flag = tryLock(LockKey);
            if (flag) {
                // 6.3.成功，开启独立线程，实现缓存重建
                System.out.println("数据已过期");
                CACHE_REBUILDER_EXECUTOR.submit(() -> {
                    try {
                        //运维的人员可能就会手动del 然后系统自动来更新 所以才会出现这样的情况
                        this.saveShop2Redis(id, 20L);
                    } catch (Exception e) {
                        throw new RuntimeException(e);
                    } finally {
                        delLock(LockKey);
                    }
                });
            }
            // 6.4.返回过期的商铺信息
            return shop;
        }

        //互斥锁
        private Shop queryWithMutex(Long id) {
            String key = CACHE_SHOP_KEY + id;
            String shopJson = stringRedisTemplate.opsForValue().get(key);
            if (StrUtil.isNotBlank(shopJson)) {
                //不是空 代表命中成功
                //String转Bean
    //            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
    //            return Result.success(shop);
                return JSONUtil.toBean(shopJson, Shop.class);
            }
            //“abc”--->true
            //null "" " \t\n"--->false
            //排除了null剩下的就是“”
            if (shopJson != null) {
                //return Result.error("店铺信息不存在！！！");
                return null;
            }

            //获取互斥锁
            String LockKey = LOCK_SHOP_KEY + id;
            Shop shop;
            try {
                boolean flag = tryLock(LockKey);
                //判断锁是否成功
                if (!flag) {
                    Thread.sleep(50);
                    //递归循环
                    return queryWithMutex(id);
                }
                //从数据库里面查
                shop = this.getById(id);
                Thread.sleep(200);
                if (shop == null) {
                    //设置redis空值
                    stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
                    //return Result.error("店铺不存在!!!已经设置成空值返回了");
                    return null;
                }
                //命中数据库之后返回给redis保存
                //Bean转String
                stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } finally {
                //释放锁
                delLock(LockKey);
            }
            return shop;
        }

        //缓存穿透
        public Shop queryWithPassThrough(Long id) {
            String key = CACHE_SHOP_KEY + id;
            String shopJson = stringRedisTemplate.opsForValue().get(key);
            if (StrUtil.isNotBlank(shopJson)) {
                //不是空 代表命中成功
                //String转Bean
    //            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
    //            return Result.success(shop);
                return JSONUtil.toBean(shopJson, Shop.class);
            }
            //“abc”--->true
            //null "" " \t\n"--->false
            //排除了null剩下的就是“”
            if (shopJson != null) {
                //return Result.error("店铺信息不存在！！！");
                return null;
            }
            //不存在 也就是未命中
            //从数据库里面查
            Shop shop = this.getById(id);
            if (shop == null) {
                //设置redis空值
                stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
                //return Result.error("店铺不存在!!!已经设置成空值返回了");
                return null;
            }
            //命中数据库之后返回给redis保存
            //Bean转String
            stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
            return shop;
        }

        //加锁
        private boolean tryLock(String key) {
            Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", LOCK_SHOP_TTL, TimeUnit.SECONDS);
            return BooleanUtil.isTrue(flag);
        }

        //释放锁
        private void delLock(String key) {
            stringRedisTemplate.delete(key);
        }

        //逻辑过期key的预热
        public void saveShop2Redis(Long id, Long expireTime) throws InterruptedException    {
            Shop shop = this.getById(id);
            Thread.sleep(200);
            RedisData redisData = new RedisData();
            redisData.setData(shop);
            redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireTime));
            //永久有效
            stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
        }
    */
    //@Transactional//Redis 操作默认不在 Spring 事务管理范围内，即使 Redis 操作失败，数据库更新依然会提交。
    //策略：
    // 1. 核心思路：用一张数据库表记录 "Redis 操作任务"，通过本地事务保证 "数据库更新 + 任务记录" 的原子性，再通过异步任务执行 Redis 操作并确认结果。
    // 2. 核心思路：通过消息队列的 "事务消息" 功能，确保 "数据库更新" 和 "Redis 操作消息" 的原子性
    // 3. 核心思路：将操作拆分为 "Try-Confirm-Cancel" 三个阶段，强制保证两边都成功或都回滚。
    @Override
    public Result update(Shop shop) {
        Long id = shop.getId();
        if (id == null) {
            return Result.error("shop的id为空！");
        }
        //操作数据库
        this.updateById(shop);
        //删除缓存
        stringRedisTemplate.delete(CACHE_SHOP_KEY + id);
        return Result.success("操作成功");
    }
}
